﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TheGunningMan
{
    namespace MdlPlugin
    {
        using System.Diagnostics;
        using System.IO;
        using System.Text.RegularExpressions;
        using Gunningine;

        /// <summary>
        /// Loader class for Quake1 models.
        /// </summary>
        public class MdlModelLoader : IResourceLoader
        {
            private ModelResource modelResource;

            private Vector3 fixOffset = new Vector3(0, 0, 24);

            private Vector3 fixScale = new Vector3(1, 1, 1);

            /// <summary>
            /// Read Mdl header.
            /// </summary>
            /// <param name="br"></param>
            /// <returns></returns>
            private MdlHeader ReadHeader(BinaryReader br)
            {
                MdlHeader header = new MdlHeader();

                header.ident = br.ReadInt32();
                header.version = br.ReadInt32();

                Debug.Assert(header.ident == 1330660425, "Invalid header (bad ident) for " + this.modelResource.Filename);
                Debug.Assert(header.version == 6, "Invalid header (bad version) for " + this.modelResource.Filename);

                // Scale
                float x = br.ReadSingle();
                float y = br.ReadSingle();
                float z = br.ReadSingle();
                header.scale = new Vector3(x, y, z);

                // Translation
                x = br.ReadSingle();
                y = br.ReadSingle();
                z = br.ReadSingle();
                header.translation = new Vector3(x, y, z);

                float boundingRadius = br.ReadSingle();

                // Eye position (not needed).
                x = br.ReadSingle();
                y = br.ReadSingle();
                z = br.ReadSingle();

                // Texture info
                header.numSkins = br.ReadInt32();
                header.skinWidth = br.ReadInt32();
                header.skinHeight = br.ReadInt32();

                header.renderWidth = MathsUtils.NextPow2(header.skinWidth);
                header.renderHeight = MathsUtils.NextPow2(header.skinHeight);

                // Geometry info
                header.numVerts = br.ReadInt32();
                header.numAdjustedVerts = header.numVerts;
                header.numTris = br.ReadInt32();
                header.numFrames = br.ReadInt32();

                Int32 syncType = br.ReadInt32();
                Int32 flags = br.ReadInt32();
                float size = br.ReadSingle();

                Debug.Assert(br.BaseStream.Position == 84, "Invalid header (bad header size) for " + this.modelResource.Filename);

                return header;
            }

            /// <summary>
            /// Load a MDL skin.
            /// </summary>
            /// <param name="br"></param>
            /// <param name="group"></param>
            /// <param name="width"></param>
            /// <param name="height"></param>
            /// <returns></returns>
            private MdlSkin ReadSkin(BinaryReader br, bool group, int width, int height)
            {
                MdlSkin skin = new MdlSkin(width, height);

                skin.width = width;
                skin.height = height;
                skin.numFrames = group ? br.ReadInt32() : 1;

                if (group)
                {
                    skin.numFrames = br.ReadInt32();
                    for (int i = 0; i < skin.numFrames; ++i)
                        skin.times.Add(br.ReadSingle());
                }
                else
                {
                    skin.numFrames = 1;
                    skin.times.Add(0);
                }

                for (int i = 0; i < skin.numFrames; ++i)
                    skin.frames.Add(br.ReadBytes(width * height));

                return skin;
            }

            /// <summary>
            /// Read texture coordinate.
            /// </summary>
            /// <param name="br"></param>
            /// <returns></returns>
            private MdlTexCoord ReadTexCoord(BinaryReader br)
            {
                MdlTexCoord tc = new MdlTexCoord();

                tc.onSeam = br.ReadInt32();
                tc.s = br.ReadInt32();
                tc.t = br.ReadInt32();

                return tc;
            }

            /// <summary>
            /// Read triangle.
            /// </summary>
            /// <param name="br"></param>
            /// <returns></returns>
            private MdlTriangle ReadTriangle(BinaryReader br)
            {
                MdlTriangle tri = new MdlTriangle(0);

                tri.facesFront = br.ReadInt32();
                tri.vertices[0] = br.ReadInt32();
                tri.vertices[1] = br.ReadInt32();
                tri.vertices[2] = br.ReadInt32();

                return tri;
            }

            /// <summary>
            /// Read vertex.
            /// </summary>
            /// <param name="br"></param>
            /// <returns></returns>
            private MdlVertex ReadVertex(BinaryReader br)
            {
                MdlVertex vert = new MdlVertex();

                vert.x = br.ReadByte();
                vert.y = br.ReadByte();
                vert.z = br.ReadByte();
                byte normal = br.ReadByte();

                return vert;
            }

            /// <summary>
            /// Read frame.
            /// </summary>
            /// <param name="br"></param>
            /// <param name="numVerts"></param>
            /// <param name="backfacing"></param>
            /// <returns></returns>
            private MdlFrame ReadFrame(BinaryReader br, int numVerts, List<Int32> backfacing)
            {
                MdlFrame frame = new MdlFrame(numVerts + backfacing.Count);

                Int32 type = br.ReadInt32();
                if (type != 0)
                    throw new NotImplementedException("Cannot load MDL files with group frames!");

                frame.bbMin = ReadVertex(br);
                frame.bbMax = ReadVertex(br);

                char[] name = new char[16];

                int nameLength = -1;
                for (int i = 0; i < 16; ++i)
                {
                    name[i] = unchecked((char)br.ReadByte());
                    if (name[i] == '\0' && nameLength < 0)
                        nameLength = i;
                }

                frame.name = new String(name, 0, nameLength);

                for (int i = 0; i < numVerts; ++i)
                    frame.vertices[i] = ReadVertex(br);

                // Add in backfacing vertices.
                for (int i = 0; i < backfacing.Count; ++i)
                    frame.vertices[numVerts + i] = frame.vertices[backfacing[i]];

                return frame;
            }

            /// <summary>
            /// Comparer for sorting MdlFrames.
            /// </summary>
            internal class MdlFrameComparer : IComparer<MdlFrame>
            {
                public int Compare(MdlFrame a, MdlFrame b)
                {
                    Match matcha = Regex.Match(a.name, @"([a-zA-Z]*)(\d*)");
                    Match matchb = Regex.Match(b.name, @"([a-zA-Z]*)(\d*)");

                    string namea = matcha.Groups[1].ToString();
                    string ida = matcha.Groups[2].ToString();
                    int inta = ida == string.Empty ? 0 : Int32.Parse(ida);

                    string nameb = matchb.Groups[1].ToString();
                    string idb = matchb.Groups[2].ToString();
                    int intb = idb == string.Empty ? 0 : Int32.Parse(idb);

                    return (namea == nameb) ? inta.CompareTo(intb) : namea.CompareTo(nameb);
                }
            }

            /// <summary>
            /// Create a texture from Mdl skin.
            /// </summary>
            /// <param name="res"></param>
            /// <param name="skin"></param>
            /// <param name="width"></param>
            /// <param name="height"></param>
            void CreateTextureFromSkin(MdlSkin skin, int width, int height)
            {
                // Load in palette file
                string fileName = "Resources/Models/quake.lmp";
                using (BinaryReader br = new BinaryReader(File.Open(fileName, FileMode.Open)))
                {
                    byte[] palette = br.ReadBytes(768);

                    // Create texture and initialise to black
                    byte[] imageData = new byte[width * height * 3];
                    for (int i = 0; i < width * height * 3; ++i)
                        imageData[i] = 0;

                    // Copy image to top left corner of texture.
                    byte[] srcData = skin.frames[0];
                    int dst = 0, src = 0;
                    for (int y = 0; y < skin.height; ++y)
                    {
                        for (int x = 0; x < skin.width; ++x)
                        {
                            // Swap RGB to BGR
                            imageData[dst + 0] = palette[srcData[src] * 3 + 2];
                            imageData[dst + 1] = palette[srcData[src] * 3 + 1];
                            imageData[dst + 2] = palette[srcData[src] * 3 + 0];

                            src += 1;
                            dst += 3;
                        }

                        dst += (width - skin.width) * 3;
                    }

                    ModelResource.Texture mt = new ModelResource.Texture();
                    mt.width = width;
                    mt.height = height;
                    mt.channels = 3;
                    mt.data = imageData;

                    this.modelResource.Textures.Add(mt);
                }
            }

            #region IModelLoader implementation

            public string Name
            {
                get { return "Mdl"; }
            }

            public string[] SupportedFormats
            {
                get { return new string[] { ".mdl" }; }
            }

            public void Load(Resource res)
            {
                this.modelResource = (ModelResource)res;
                using (BinaryReader br = new BinaryReader(File.Open(this.modelResource.Filename, FileMode.Open)))
                {
                    // Read in header
                    MdlHeader header = ReadHeader(br);

                    // Read in skins
                    List<MdlSkin> skins = new List<MdlSkin>();
                    for (int i = 0; i < header.numSkins; ++i)
                    {
                        Int32 group = br.ReadInt32();
                        skins.Add(ReadSkin(br, group != 0, header.skinWidth, header.skinHeight));
                    }

                    // Read in texcoords.  We keep them in texture space rather than in unit space,
                    // because we will probably need to change the texture size later (as most MDL
                    // skins are not pow-2), so this saves us some calculations.
                    List<MdlTexCoord> texCoords = new List<MdlTexCoord>();
                    for (int i = 0; i < header.numVerts; ++i)
                        texCoords.Add(ReadTexCoord(br));

                    // Read in triangle data.  
                    List<MdlTriangle> triangles = new List<MdlTriangle>();
                    List<Int32> vertsToCopy = new List<int>();
                    for (int i = 0; i < header.numTris; ++i)
                    {
                        MdlTriangle tri = ReadTriangle(br);

                        // We may need to split vertices which are not front-facing
                        // so keep track of them here.
                        if (tri.facesFront == 0)
                        {
                            for (int j = 0; j < 3; ++j)
                            {
                                if (texCoords[tri.vertices[j]].onSeam != 0)
                                {
                                    // Add a new texcoord
                                    MdlTexCoord tc = new MdlTexCoord();
                                    tc.s = texCoords[tri.vertices[j]].s + (int)(header.skinWidth * 0.5f);
                                    tc.t = texCoords[tri.vertices[j]].t;
                                    tc.onSeam = 0;

                                    texCoords.Add(tc);

                                    // Set this vertex to be copied when we read in frame data.
                                    vertsToCopy.Add(tri.vertices[j]);

                                    // Update triangle to use new vert
                                    tri.vertices[j] = header.numAdjustedVerts;
                                    header.numAdjustedVerts++;
                                }
                            }
                        }

                        triangles.Add(tri);
                    }

                    this.modelResource.NumVertices = header.numAdjustedVerts;

                    // Read in frame data.
                    List<MdlFrame> frames = new List<MdlFrame>();
                    for (int i = 0; i < header.numFrames; ++i)
                        frames.Add(ReadFrame(br, header.numVerts, vertsToCopy));

                    Debug.Assert(br.BaseStream.Position == br.BaseStream.Length, "Did not reach end of file when reading " + this.modelResource.Filename);

                    // Create texture(s).
                    foreach (MdlSkin mdlSkin in skins)
                    {
                        CreateTextureFromSkin(mdlSkin, header.renderWidth, header.renderHeight);
                    }

                    // Create mesh data
                    foreach (MdlTriangle mdlTri in triangles)
                    {
                        ModelResource.Triangle mdTri = new ModelResource.Triangle();
                        for (int i = 0; i < 3; ++i)
                            mdTri.vertices[i] = mdlTri.vertices[i];

                        this.modelResource.Triangles.Add(mdTri);
                    }

                    // Collection animation data together.  Frames are named <anim><n> where anim is the name
                    // and n is the frame index.  First off, sort the frames by name, then iterate through.
                    frames.Sort(new MdlFrameComparer());

                    string curAnimName = String.Empty;
                    ModelResource.Animation curAnim = null;
                    foreach (MdlFrame mdlFrame in frames)
                    {
                        Match match = Regex.Match(mdlFrame.name, @"([a-zA-Z]*)(\d*)");

                        string nextAnimName = match.Groups[1].ToString();

                        string idStr = match.Groups[2].ToString();
                        int animId = idStr == String.Empty ? 1 : Int32.Parse(idStr);

                        ModelResource.Frame mdFrame = new ModelResource.Frame();

                        // Calculate size based on bounding box for this frame.
                        float sizeX = Math.Abs((mdlFrame.bbMax.x * header.scale.X + header.translation.X)
                                             - (mdlFrame.bbMin.x * header.scale.X + header.translation.X));
                        float sizeY = Math.Abs((mdlFrame.bbMax.y * header.scale.Y + header.translation.Y)
                                             - (mdlFrame.bbMin.y * header.scale.Y + header.translation.Y));

                        // This is an approximation!
                        mdFrame.size = sizeX > sizeY ? sizeX / 8.0f : sizeY / 8.0f;

                        for (int i = 0; i < header.numAdjustedVerts; ++i)
                        {
                            MdlVertex mdlVertex = mdlFrame.vertices[i];
                            ModelResource.Vertex mdVertex = new ModelResource.Vertex();

                            // Position
                            mdVertex.p = new Vector3(mdlVertex.x * header.scale.X + header.translation.X,
                                                     mdlVertex.y * header.scale.Y + header.translation.Y,
                                                     mdlVertex.z * header.scale.Z + header.translation.Z);

                            // Fix offset and scale (based on QuakeC values).
                            mdVertex.p += this.fixOffset;

                            switch (this.Name)
                            {
                                case "Models/demon":
                                    mdVertex.p *= 0.8f;
                                    break;
                                case "Models/shalrath":
                                    mdVertex.p *= 0.7f;
                                    break;
                                default:
                                    mdVertex.p *= 1.0f;
                                    break;
                            }

                            // Vertex normal?
                            // TODO: ...
                            mdVertex.n = Vector3.Zero;

                            // Texture coords.  If we are on a back-facing triangle at a seam,
                            // then adjust the coords.
                            mdVertex.u = texCoords[i].s / (float)header.renderWidth;
                            mdVertex.v = texCoords[i].t / (float)header.renderHeight;

                            // Colour
                            mdVertex.r = 1.0f;
                            mdVertex.g = 1.0f;
                            mdVertex.b = 1.0f;
                            mdVertex.a = 1.0f;

                            mdFrame.vertices.Add(mdVertex);
                        }

                        // If the name has changed, create a new animation.
                        if (nextAnimName != curAnimName)
                        {
                            curAnimName = nextAnimName;
                            curAnim = new ModelResource.Animation();
                            this.modelResource.Animations[curAnimName] = curAnim;
                            Logger.InfoFormat("Animation: {0}", curAnimName);
                        }

                        curAnim.frames.Add(mdFrame);
                    }
                }
            }

            #endregion
        }
    }
}
